#       main.py
#       
#       Copyright 2011 Alexey Zotov <alexey.zotov@gmail.com>
#       
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

# -*- coding: utf-8 -*-

import math

from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4 import QtGui

from PyKDE4 import kdecore
from PyKDE4.plasma import Plasma
from PyKDE4 import plasmascript

import mdstat
import config

class SoftRAIDMonitor(plasmascript.Applet):
    HAS_DEGRADED = ('raid1', 'raid4', 'raid5', 'raid6', 'raid10')
    
    def __init__(self, parent, args=None):
        plasmascript.Applet.__init__(self, parent)

    def init(self):
        self.setHasConfigurationInterface(True)
        self.configChanged()

        self.raidStatus = None

        self.hddIcon = QtGui.QIcon.fromTheme('drive-harddisk')
        self.errorIcon = QtGui.QIcon.fromTheme('dialog-error')
        self.warningIcon = QtGui.QIcon.fromTheme('dialog-warning')

        self.tooltipContent = Plasma.ToolTipContent(
            self.device,
            '',
            self.hddIcon
        )
        Plasma.ToolTipManager.self().setContent(
            self.applet, self.tooltipContent)

        if self.containment().containmentType() in (
                Plasma.Containment.PanelContainment,
                Plasma.Containment.CustomPanelContainment
            ):
                self.setAspectRatioMode(Plasma.Square)

        self.pollingTimer = self.startTimer(5000)
        self.updateStatus()

    def configChanged(self):
        cg = self.config()

        self.device = str(cg.readEntry('device', '').toString())
        if not self.device:
            self.setConfigurationRequired(True)

        self.colorActive = QtGui.QColor(cg.readEntry(
            'colorActive', '#00FF00').toString())
        self.colorResync = QtGui.QColor(cg.readEntry(
            'colorResync', '#FFFF00').toString())
        self.colorFailed = QtGui.QColor(cg.readEntry(
            'colorFailed', '#FF0000').toString())
        self.colorInactive = QtGui.QColor(cg.readEntry(
            'colorInactive', '#7F7F7F').toString())

    def print_device(self, dev, device):
        print '    device:', dev
        print '        active:', device['active']
        print '        read_only:', device['read_only']
        print '        pers:', device['pers']
        if device['pers'] in ('raid1', 'raid4', 'raid5', 'raid6', 'raid10'):
            print '            total:', device['raid']['total']
            print '            degraded:', device['raid']['degraded']
            print '            status:', device['raid']['status']
        if device['pers'] in ('raid4', 'raid5', 'raid6'):
            print '            level:', device['raid']['level']
            print '            chunk:', device['raid']['chunk']
            print '            algorithm:', device['raid']['algorithm']
        if device['pers'] == 'raid10':
            if 'chunk' in device['raid']:
                print '            chunk:', device['raid']['chunk']
            if 'near-copies' in device['raid']:
                print '            near-copies:', device['raid']['near-copies']
            if 'offset-copies' in device['raid']:
                print '            offset-copies:', \
                    device['raid']['offset-copies']
            if 'far-copies' in device['raid']:
                print '            far-copies:', device['raid']['far-copies']
        print '        disks:'
        for disk_number in sorted(device['disks']):
            print '            disk:', disk_number
            print '                name:', device['disks'][disk_number]['name']
            print '                type:', device['disks'][disk_number]['type']
        print '        blocks:', device['blocks']
        print '        super:', device['super']
        if device['resync']['type']:
            print '        resync:'
            print '            type:', device['resync']['type']
            print '            finish:', device['resync']['finish']
            if device['resync']['finish'] not in ('DELAYED', 'PENDING'):
                print '            percent:', device['resync']['percent']
                print '            blocks:', device['resync']['blocks']
                print '            max_blocks:', device['resync']['max_blocks']
                print '            speed:', device['resync']['speed']

    def createConfigurationInterface(self, parent):
        self.configUi = config.Config()
        parent.addPage(self.configUi, kdecore.i18n('General'), self.icon())
        parent.applyClicked.connect(self.configAccepted)
        parent.okClicked.connect(self.configAccepted)

        status = mdstat.get_status()
        for dev in sorted(status['devices']):
            self.configUi.deviceComboBox.addItem(dev)
            self.print_device(dev, status['devices'][dev])

        self.configUi.deviceComboBox.setCurrentIndex(
            self.configUi.deviceComboBox.findText(self.device))

        self.configUi.colorActiveButton.setColor(self.colorActive)
        self.configUi.colorResyncButton.setColor(self.colorResync)
        self.configUi.colorFailedButton.setColor(self.colorFailed)
        self.configUi.colorInactiveButton.setColor(self.colorInactive)

        self.configUi.changed.connect(parent.settingsModified)

    def configAccepted(self):
        cg = self.config()
        
        self.device = str(self.configUi.deviceComboBox.currentText())
        cg.writeEntry('device', self.device)

        self.colorActive = self.configUi.colorActiveButton.color()
        self.colorResync = self.configUi.colorResyncButton.color()
        self.colorFailed = self.configUi.colorFailedButton.color()
        self.colorInactive = self.configUi.colorInactiveButton.color()
        
        cg.writeEntry('colorActive', self.colorActive.name())
        cg.writeEntry('colorResync', self.colorResync.name())
        cg.writeEntry('colorFailed', self.colorFailed.name())
        cg.writeEntry('colorInactive', self.colorInactive.name())
            
        self.emit(QtCore.SIGNAL('configNeedsSaving()'))

        if self.device:
            self.setConfigurationRequired(False)

        self.updateStatus()

    def getFont(self, painter, rect, text):
        font = painter.font()
        pointSize = 1
        while True:
            pointSize += 1
            font.setPointSize(pointSize)
            fontMetrics = QtGui.QFontMetrics(font)
            if fontMetrics.height() > rect.height():
                break
            if fontMetrics.width(text) > rect.width():
                break

        pointSize -= 1
        font.setPointSize(pointSize)
        return font

    def paintInterface(self, painter, option, rect):
        if not self.device:
            return

        if not self.raidStatus:
            painter.drawPixmap(rect, self.errorIcon.pixmap(rect.size()))

            self.tooltipContent.setSubText(
                '<font color="%s">device %s not found in /proc/mdstat</font>' \
                    % (self.colorFailed.name(), self.device)
            )
            Plasma.ToolTipManager.self().setContent(
                self.applet, self.tooltipContent)

            return

        if self.containment().containmentType() in (
                Plasma.Containment.PanelContainment,
                Plasma.Containment.CustomPanelContainment
            ):
                self.paintOnPanel(painter, option, rect)
                return

        theme = Plasma.Theme.defaultTheme()

        lines = 1 + len(self.raidStatus['disks'])
        if self.raidStatus['resync']['type']:
            lines += 1

        lineHeight = rect.height() // lines
        iconOffset = lineHeight // 10
        iconHeight = lineHeight - 2 * iconOffset

        raidIconRect = QtCore.QRect(
            rect.topLeft() + QtCore.QPoint(iconOffset, iconOffset),
            QtCore.QSize(iconHeight, iconHeight)
        )
        raidNameRect = QtCore.QRect(
            rect.topLeft() + QtCore.QPoint(lineHeight + iconOffset, iconOffset),
            QtCore.QSize(
                rect.width() - lineHeight - iconOffset,
                iconHeight // 2
            )
        )
        raidStatusRect = raidNameRect.translated(0, iconHeight // 2)
        
        painter.save()
        painter.setPen(theme.color(Plasma.Theme.TextColor))

        painter.save()
        if not self.raidStatus['active']:
            painter.setOpacity(0.5)
        
        raidIconOffset = iconHeight // 10
        painter.drawPixmap(
            raidIconRect.translated(0, raidIconOffset),
            self.hddIcon.pixmap(raidIconRect.size())
        )
        painter.drawPixmap(
            raidIconRect.translated(0, -raidIconOffset),
            self.hddIcon.pixmap(raidIconRect.size())
        )

        if self.raidStatus['pers'] in self.HAS_DEGRADED \
            and self.raidStatus['raid']['degraded'] > 0:
                iconRect = QtCore.QRect(
                    raidIconRect.center(),
                    raidIconRect.size() / 2
                )

                if self.raidStatus['resync']['type']:
                    icon = self.warningIcon
                else:
                    icon = self.errorIcon
                    
                painter.drawPixmap(
                    iconRect,
                    icon.pixmap(iconRect.size())
                )
        
        painter.restore()
        
        if self.raidStatus['active']:
            pers = self.raidStatus['pers']
        else:
            pers = 'inactive'
        painter.setFont(self.getFont(painter, raidNameRect,
            self.device + ': ' + pers))
            
        boundingRect = painter.drawText(raidNameRect, Qt.AlignVCenter,
            self.device + ': ')

        raidNameRect.translate(boundingRect.width(), 0)
        painter.save()
        if self.raidStatus['active']:            
            painter.setPen(self.colorActive)
            if self.raidStatus['pers'] in self.HAS_DEGRADED \
                and self.raidStatus['raid']['degraded'] > 0:
                    if self.raidStatus['resync']['type']:
                        painter.setPen(self.colorResync)
                    else:
                        painter.setPen(self.colorFailed)
        else:
            painter.setPen(self.colorInactive)
        painter.drawText(raidNameRect, Qt.AlignVCenter, pers)
        painter.restore()

        statusText = ''
        if self.raidStatus['pers'] in self.HAS_DEGRADED:
            nondegraded = self.raidStatus['raid']['total'] - \
                self.raidStatus['raid']['degraded']
            statusText = '[%s/%s] [%s]' % (
                self.raidStatus['raid']['total'],
                nondegraded,
                self.raidStatus['raid']['status']
            )
        painter.setFont(self.getFont(painter, raidStatusRect, statusText))
        painter.drawText(raidStatusRect, Qt.AlignVCenter, statusText)

        for ind, disk_number in enumerate(sorted(self.raidStatus['disks'])):
            diskRectOffset = QtCore.QPoint(0, (1 + ind) * lineHeight)
            diskIconOffset = diskRectOffset + \
                QtCore.QPoint(2 * iconOffset, iconOffset)
            diskNameOffset = diskRectOffset + \
                QtCore.QPoint(
                    lineHeight + iconOffset * 2,
                    iconOffset * 2
                )
            diskIconRect = QtCore.QRect(
                rect.topLeft() + diskIconOffset,
                QtCore.QSize(iconHeight, iconHeight)
            )
            diskNameRect = QtCore.QRect(
                rect.topLeft() + diskNameOffset,
                QtCore.QSize(
                    rect.width() - lineHeight - iconOffset * 2,
                    (iconHeight - iconOffset * 2) // 2
                )
            )
            diskStatusRect = diskNameRect.translated(0,
                (iconHeight - iconOffset * 2) // 2)

            diskType = self.raidStatus['disks'][disk_number]['type']
            writemostly = diskType.startswith('(W)')
            if writemostly:
                diskType = diskType[3:]
            
            painter.save()
            if diskType == '(S)':
                painter.setOpacity(0.5)
            painter.drawPixmap(
                diskIconRect,
                self.hddIcon.pixmap(diskIconRect.size())
            )
            if diskType == '(F)':
                errorIconRect = QtCore.QRect(
                    diskIconRect.center(),
                    diskIconRect.size() / 2
                )
                painter.drawPixmap(
                    errorIconRect,
                    self.errorIcon.pixmap(errorIconRect.size())
                )
            painter.restore()

            diskName = self.raidStatus['disks'][disk_number]['name']
            painter.setFont(self.getFont(painter, diskNameRect, diskName))
            painter.drawText(diskNameRect, Qt.AlignVCenter, diskName)

            painter.save()
            diskStatus = 'w.m. ' if writemostly else ''
            if diskType == '(F)':
                diskStatus += 'failed'
                painter.setPen(self.colorFailed)
            elif diskType == '(S)':
                diskStatus += 'spare'
                painter.setPen(self.colorInactive)
            else:
                painter.setPen(self.colorActive)
                diskStatus += 'active'
            painter.setFont(self.getFont(painter, diskStatusRect, diskStatus))
            painter.drawText(diskStatusRect, Qt.AlignVCenter, diskStatus)
            painter.restore()

        resyncType = self.raidStatus['resync']['type']
        if resyncType:
            resyncRectOffset = QtCore.QPoint(0, (lines - 1) * lineHeight)
            resyncRectHeight = (lineHeight - iconOffset * 2) // 2
            resyncTypeRect = QtCore.QRect(
                rect.topLeft() + resyncRectOffset + \
                    QtCore.QPoint(iconOffset, iconOffset),
                QtCore.QSize(
                    rect.width() - iconOffset * 2,
                    resyncRectHeight
                )
            )
            finishRect = resyncTypeRect.translated(0, resyncRectHeight)

            painter.setFont(self.getFont(painter, resyncTypeRect, resyncType))
            boundingRect = painter.drawText(resyncTypeRect, Qt.AlignVCenter,
                resyncType)

            percent = self.raidStatus['resync'].get('percent', -1)
            if percent >= 0:
                percentRect = resyncTypeRect.adjusted(
                    boundingRect.width() + 2, 2, -2, -2)

                painter.save()
                percentColor = theme.color(Plasma.Theme.HighlightColor)
                painter.setPen(percentColor)

                painter.drawRoundedRect(percentRect, 1, 1)

                percentLeft = (100.0 - percent) / 100.0
                percentRect.adjust(1, 1, -percentLeft * percentRect.width(), 1)

                painter.fillRect(percentRect, percentColor)
                
                painter.restore()

            finishText = 'finish: %s' % self.raidStatus['resync']['finish']
            speed = self.raidStatus['resync'].get('speed')
            if speed:
                finishText += ' | speed: %s' % speed
            painter.setFont(self.getFont(painter, finishRect, finishText))
            painter.drawText(finishRect, Qt.AlignVCenter, finishText)

        painter.restore()

    def paintOnPanel(self, painter, option, rect):
        panelSize = min(rect.width(), rect.height())
        iconOffset = panelSize // 10
        iconSize = panelSize - 2 * iconOffset

        iconRect = QtCore.QRect(
            rect.topLeft() + QtCore.QPoint(iconOffset, iconOffset),
            QtCore.QSize(iconSize, iconSize)
        )

        painter.save()
        if not self.raidStatus['active']:
            painter.setOpacity(0.5)

        raidIconOffset = iconSize // 10
        painter.drawPixmap(
            iconRect.translated(0, raidIconOffset),
            self.hddIcon.pixmap(iconRect.size())
        )
        painter.drawPixmap(
            iconRect.translated(0, -raidIconOffset),
            self.hddIcon.pixmap(iconRect.size())
        )

        if self.raidStatus['pers'] in self.HAS_DEGRADED \
            and self.raidStatus['raid']['degraded'] > 0:
                iconRect = QtCore.QRect(
                    iconRect.center(),
                    iconRect.size() / 2
                )
                if self.raidStatus['resync']['type']:
                    icon = self.warningIcon
                else:
                    icon = self.errorIcon
                painter.drawPixmap(
                    iconRect,
                    icon.pixmap(iconRect.size())
                )
        
        painter.restore()

    def updateTooltip(self):
        if self.raidStatus['active']:
            statusColor = self.colorActive
            pers = self.raidStatus['pers']
            if pers in self.HAS_DEGRADED \
                and self.raidStatus['raid']['degraded'] > 0:
                    if self.raidStatus['resync']['type']:
                        statusColor = self.colorResync
                    else:
                        statusColor = self.colorFailed
        else:
            statusColor = self.colorInactive
            pers = 'inactive'
            
        self.tooltipContent.setMainText(
            '%s:&nbsp;<font color="%s">%s</font>' % (
                self.device, statusColor.name(), pers)
        )

        lines = []
        for disk_number in sorted(self.raidStatus['disks']):
            diskName = self.raidStatus['disks'][disk_number]['name']
            diskType = self.raidStatus['disks'][disk_number]['type']
            diskStatus = ''
            if diskType.startswith('(W)'):
                diskStatus = 'w.m. '
                diskType = diskType[3:]
            if diskType == '(F)':
                statusColor = self.colorFailed
                diskStatus += 'failed'
            elif diskType == '(S)':
                statusColor = self.colorInactive
                diskStatus += 'spare'
            else:
                statusColor = self.colorActive
                diskStatus += 'active'
            lines.append('%s:&nbsp;<font color="%s">%s</font>' % (
                diskName, statusColor.name(), diskStatus))

        resyncType = self.raidStatus['resync']['type']
        if resyncType:
            percent = self.raidStatus['resync'].get('percent', -1)
            if percent >= 0:
                lines.append('%s:&nbsp;%3.1f%%' % (resyncType, percent))
            else:
                lines.append('%s:&nbsp;%s' % (
                    resyncType, self.raidStatus['resync']['finish']))
        
        self.tooltipContent.setSubText('<br>'.join(lines))
        
        Plasma.ToolTipManager.self().setContent(
            self.applet, self.tooltipContent)

    def timerEvent(self, event):
        super(SoftRAIDMonitor, self).timerEvent(event)

        if event.timerId() == self.pollingTimer:
            self.updateStatus()

    def updateStatus(self):
        self.raidStatus = None

        if not self.device:
            return

        status = mdstat.get_status()

        if self.device not in status['devices']:
            print self.device, 'not in devices'
            return

        self.raidStatus = status['devices'][self.device]

        self.updateTooltip()
        self.update()


def CreateApplet(parent):
    return SoftRAIDMonitor(parent)
